{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Application - Example 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's look at some further examples of using descriptors that provides better better reusability than using `property` types (remember the repeated code issue we were trying to solve in the first place!)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We have already seen that data validation works well with descriptors.\n",
    "\n",
    "For example, we may want our object attributes to have valid values for some of it's attributes:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Int:\n",
    "    def __set_name__(self, owner_class, prop_name):\n",
    "        self.prop_name = prop_name\n",
    "        \n",
    "    def __set__(self, instance, value):\n",
    "        if not isinstance(value, int):\n",
    "            raise ValueError(f'{self.prop_name} must be an integer.')\n",
    "        instance.__dict__[self.prop_name] = value\n",
    "        \n",
    "    def __get__(self, instance, owner_class):\n",
    "        if instance is None:\n",
    "            return self\n",
    "        else:\n",
    "            return instance.__dict__.get(self.prop_name, None)\n",
    "            "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Float:\n",
    "    def __set_name__(self, owner_class, prop_name):\n",
    "        self.prop_name = prop_name\n",
    "        \n",
    "    def __set__(self, instance, value):\n",
    "        if not isinstance(value, float):\n",
    "            raise ValueError(f'{self.prop_name} must be a float.')\n",
    "        instance.__dict__[self.prop_name] = value\n",
    "        \n",
    "    def __get__(self, instance, value):\n",
    "        if instance is None:\n",
    "            return self\n",
    "        else:\n",
    "            return instance.__dict__.get(self.prop_name, None)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "class List:\n",
    "    def __set_name__(self, owner_class, prop_name):\n",
    "        self.prop_name = prop_name\n",
    "        \n",
    "    def __set__(self, instance, value):\n",
    "        if not isinstance(value, list):\n",
    "            raise ValueError(f'{self.prop_name} must be a list.')\n",
    "        instance.__dict__[self.prop_name] = value\n",
    "        \n",
    "    def __get__(self, instance, value):\n",
    "        if instance is None:\n",
    "            return self\n",
    "        else:\n",
    "            return instance.__dict__.get(self.prop_name, None)\n",
    "        \n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can now use these descriptors in multiple class definitions, and as many times as we want in each class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    age = Int()\n",
    "    height = Float()\n",
    "    tags = List()\n",
    "    favorite_foods = List()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "age must be an integer.\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    p.age = 12.5\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "height must be a float.\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    p.height = 'abc'\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tags must be a list.\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    p.tags = 'python'\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One thing here, is that I got rather tired of writing the same code multiple times for the descriptor classes! (beats having to re-write the same code over and over again that we would have had with properties, but still, we can do better than that!)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So let's rewrite this to be a bit more generic:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ValidType:\n",
    "    def __init__(self, type_):\n",
    "        self._type = type_\n",
    "        \n",
    "    def __set_name__(self, owner_clasds, prop_name):\n",
    "        self.prop_name = prop_name\n",
    "        \n",
    "    def __set__(self, instance, value):\n",
    "        if not isinstance(value, self._type):\n",
    "            raise ValueError(f'{self.prop_name} must be of type '\n",
    "                             f'{self._type.__name__}'\n",
    "                            )\n",
    "        instance.__dict__[self.prop_name] = value\n",
    "        \n",
    "    def __get__(self, instance, owner_class):\n",
    "        if instance is None:\n",
    "            return self\n",
    "        else:\n",
    "            return instance.__dict__.get(self.prop_name, None)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And now we can achieve the same functionality as before:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    age = ValidType(int)\n",
    "    height = ValidType(float)\n",
    "    tags = ValidType(list)\n",
    "    favorite_foods = ValidType(tuple)\n",
    "    name = ValidType(str)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "age must be of type int\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    p.age = 10.5\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "height must be of type float\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    p.height = 10\n",
    "except ValueError as ex:\n",
    "    print(ex)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now I'd like to allow setting the height to an integer value, since those are a subset of floats (in the mathematicel sense). That's easy, all I need to do is to use the `numbers.Real` class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numbers"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "isinstance(10.1, numbers.Real)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "isinstance(10, numbers.Real)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So let's tweak our `Person` class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    age = ValidType(int)\n",
    "    height = ValidType(numbers.Real)\n",
    "    tags = ValidType(list)\n",
    "    favorite_foods = ValidType(tuple)\n",
    "    name = ValidType(str)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "p.height = 10"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "10"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.height"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
